fxrs-process 0.0.7

Process, PTY, and terminal lifecycle services for fxrs
Documentation
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

use fx_core::{CancellationSignal, ToolError, ToolOutput};

use crate::monitor::MonitorOperation;
use crate::native_terminal::{
    ClosePolicy, NamedKey, SessionBackend, StartSpec, TerminalSessionHost, TerminalSignal,
    WaitCondition, WritePayload, failure_output,
};
use crate::terminal_host_protocol::{
    ReturnCondition, TerminalBackend, TerminalClosePolicy, TerminalKey, TerminalOperation,
    TerminalSignal as WireSignal, TerminalWrite,
};
use crate::terminal_host_server::{HostClient, HostServerConfig, HostServerError};
use crate::tmux_terminal::TmuxTerminalHost;

const NATIVE_BACKENDS: &[&str] = &["native"];
const NATIVE_AND_TMUX_BACKENDS: &[&str] = &["native", "tmux"];

#[derive(Clone, Debug)]
pub(crate) struct HostedTerminalHost {
    config: HostServerConfig,
    executable: PathBuf,
    tmux_available: bool,
}

impl HostedTerminalHost {
    pub(crate) fn discover() -> Option<Self> {
        if std::env::var_os("FX_TERMINAL_HOST_DISABLE").is_some() {
            return None;
        }
        let executable = std::env::var_os("FX_TERMINAL_HOST_EXE")
            .map(PathBuf::from)
            .or_else(discover_sibling)?
            .canonicalize()
            .ok()?;
        if !executable.is_file() {
            return None;
        }
        Some(Self {
            config: HostServerConfig::from_environment().ok()?,
            executable,
            tmux_available: TmuxTerminalHost::discover().is_some(),
        })
    }

    fn client(&self) -> Result<HostClient, ToolError> {
        HostClient::connect_or_spawn(&self.config, &self.executable).map_err(map_host_error)
    }

    fn call(&self, operation: TerminalOperation) -> Result<ToolOutput, ToolError> {
        self.client()?.terminal(operation).map_err(map_host_error)
    }

    fn call_cancellable(
        &self,
        operation: TerminalOperation,
        cancellation: Arc<dyn CancellationSignal>,
    ) -> Result<ToolOutput, ToolError> {
        self.client()?
            .terminal_cancellable(operation, cancellation)
            .map_err(map_host_error)
    }
}

impl TerminalSessionHost for HostedTerminalHost {
    fn backends(&self) -> &'static [&'static str] {
        if self.tmux_available {
            NATIVE_AND_TMUX_BACKENDS
        } else {
            NATIVE_BACKENDS
        }
    }

    fn start(
        &self,
        spec: StartSpec,
        cancellation: Arc<dyn CancellationSignal>,
    ) -> Result<ToolOutput, ToolError> {
        if cancellation.is_cancelled() {
            return Ok(failure_output("start", None, "cancelled", false));
        }
        self.call_cancellable(
            TerminalOperation::Start {
                backend: map_backend(spec.backend),
                workspace_root: spec.workspace_root,
                cwd: spec.cwd,
                initial_monitors: spec.initial_monitors,
                command: spec.command,
                shell: spec.shell,
                arguments: spec.arguments,
                sandbox_profile: spec.sandbox_profile,
                rows: spec.rows,
                columns: spec.columns,
                return_when: spec.return_when.map(map_return),
                wait_ceiling_ms: spec.wait_ceiling.map(duration_ms),
            },
            cancellation,
        )
    }

    fn read(&self, session_id: &str, segment: u64, offset: u64) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::Read {
            session_id: session_id.into(),
            cursor_segment: segment,
            cursor_offset: offset,
        })
    }

    fn screen(&self, session_id: &str) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::Screen {
            session_id: session_id.into(),
        })
    }

    fn write(&self, session_id: &str, payload: WritePayload) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::Write {
            session_id: session_id.into(),
            write: map_write(payload),
        })
    }

    fn wait(
        &self,
        session_id: &str,
        condition: WaitCondition,
        ceiling: Duration,
        cancellation: Arc<dyn CancellationSignal>,
    ) -> Result<ToolOutput, ToolError> {
        self.call_cancellable(
            TerminalOperation::Wait {
                session_id: session_id.into(),
                return_when: map_return(condition),
                wait_ceiling_ms: duration_ms(ceiling),
            },
            cancellation,
        )
    }

    fn inspect(
        &self,
        session_id: &str,
        after_event_id: Option<u64>,
        acknowledge_event_id: Option<u64>,
        max_events: usize,
    ) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::Inspect {
            session_id: session_id.into(),
            after_event_id,
            acknowledge_event_id,
            max_events: u16::try_from(max_events).unwrap_or(u16::MAX),
        })
    }

    fn monitor(
        &self,
        session_id: &str,
        operation: MonitorOperation,
    ) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::Monitor {
            session_id: session_id.into(),
            operation,
        })
    }

    fn list(&self, backend: Option<SessionBackend>) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::List {
            backend: backend.map(map_backend),
        })
    }

    fn resize(&self, session_id: &str, rows: u16, columns: u16) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::Resize {
            session_id: session_id.into(),
            rows,
            columns,
        })
    }

    fn signal(&self, session_id: &str, signal: TerminalSignal) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::Signal {
            session_id: session_id.into(),
            signal: map_signal(signal),
        })
    }

    fn close(&self, session_id: &str, policy: ClosePolicy) -> Result<ToolOutput, ToolError> {
        self.call(TerminalOperation::Close {
            session_id: session_id.into(),
            close_policy: match policy {
                ClosePolicy::Graceful => TerminalClosePolicy::Graceful,
                ClosePolicy::Force => TerminalClosePolicy::Force,
            },
        })
    }
}

fn discover_sibling() -> Option<PathBuf> {
    let executable = std::env::current_exe().ok()?;
    let parent = executable.parent()?;
    [
        parent.join("fx-terminal-host"),
        parent.parent()?.join("fx-terminal-host"),
    ]
    .into_iter()
    .find(|candidate| candidate.is_file())
}

fn duration_ms(duration: Duration) -> u64 {
    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}

fn map_backend(backend: SessionBackend) -> TerminalBackend {
    match backend {
        SessionBackend::Native => TerminalBackend::Native,
        SessionBackend::Tmux => TerminalBackend::Tmux,
    }
}

fn map_return(condition: WaitCondition) -> ReturnCondition {
    match condition {
        WaitCondition::Started => ReturnCondition::Started,
        WaitCondition::Exit => ReturnCondition::Exit,
        WaitCondition::Quiet(duration) => ReturnCondition::Quiet {
            duration_ms: duration_ms(duration),
        },
        WaitCondition::Match(pattern) => ReturnCondition::Match { pattern },
    }
}

fn map_write(payload: WritePayload) -> TerminalWrite {
    match payload {
        WritePayload::Text(text) => TerminalWrite::Text { text },
        WritePayload::Paste(text) => TerminalWrite::Paste { text },
        WritePayload::Keys(keys) => TerminalWrite::Keys {
            keys: keys.into_iter().map(map_key).collect(),
        },
        WritePayload::Controls(controls) => TerminalWrite::Controls { controls },
    }
}

fn map_key(key: NamedKey) -> TerminalKey {
    match key {
        NamedKey::Enter => TerminalKey::Enter,
        NamedKey::Tab => TerminalKey::Tab,
        NamedKey::Escape => TerminalKey::Escape,
        NamedKey::Backspace => TerminalKey::Backspace,
        NamedKey::Delete => TerminalKey::Delete,
        NamedKey::Insert => TerminalKey::Insert,
        NamedKey::ArrowUp => TerminalKey::ArrowUp,
        NamedKey::ArrowDown => TerminalKey::ArrowDown,
        NamedKey::ArrowLeft => TerminalKey::ArrowLeft,
        NamedKey::ArrowRight => TerminalKey::ArrowRight,
        NamedKey::Home => TerminalKey::Home,
        NamedKey::End => TerminalKey::End,
        NamedKey::PageUp => TerminalKey::PageUp,
        NamedKey::PageDown => TerminalKey::PageDown,
    }
}

fn map_signal(signal: TerminalSignal) -> WireSignal {
    match signal {
        TerminalSignal::Hangup => WireSignal::Hangup,
        TerminalSignal::Interrupt => WireSignal::Interrupt,
        TerminalSignal::Quit => WireSignal::Quit,
        TerminalSignal::Terminate => WireSignal::Terminate,
        TerminalSignal::Kill => WireSignal::Kill,
    }
}

fn map_host_error(error: HostServerError) -> ToolError {
    ToolError::Execution(error.to_string())
}