use std::{
sync::{Arc, Mutex},
time::Duration,
};
use anyhow::{Result, bail};
use async_trait::async_trait;
use ghosttea_text::TextEngine;
use serde::Serialize;
use tokio::sync::broadcast;
use crate::{
FrameHub, TerminalPresentationConfig,
service::Registry,
session::{SessionActivity, SessionStatus, SessionSummary},
tunnel_protocol::{SharedSessionSummary, TunnelInput},
};
#[derive(Clone, Debug)]
pub struct RemoteControlClaim {
pub controller_view_id: String,
pub control_epoch: u64,
pub cols: u16,
pub rows: u16,
pub layout_epoch: u64,
}
#[derive(Clone, Debug)]
pub struct RemoteControlChanged {
pub session_id: String,
pub controller_view_id: String,
pub control_epoch: u64,
pub cols: u16,
pub rows: u16,
pub layout_epoch: u64,
}
#[derive(Clone, Debug)]
pub struct RemoteActivityChanged {
pub session_id: String,
pub activity: SessionActivity,
}
pub struct RemoteResize {
pub attachment_epoch: u64,
pub control_epoch: u64,
pub resize_sequence: u64,
pub cols: u16,
pub rows: u16,
}
pub struct RemoteSelection {
pub attachment_epoch: u64,
pub start_column: u16,
pub start_row: u32,
pub end_column: u16,
pub end_row: u32,
pub select_all: bool,
}
pub struct RemoteAttachment {
pub attachment_epoch: u64,
pub read_write: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RemoteLifecycleState {
Opening,
Live,
Synchronizing,
Reconnecting,
Suspended,
Ended,
}
impl RemoteLifecycleState {
pub fn as_str(self) -> &'static str {
match self {
Self::Opening => "opening",
Self::Live => "live",
Self::Synchronizing => "synchronizing",
Self::Reconnecting => "reconnecting",
Self::Suspended => "suspended",
Self::Ended => "ended",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RemoteEndedReason {
SessionClosed,
SessionExited,
SessionUnavailable,
HostRestarted,
HostShutdown,
ClosedLocally,
}
impl RemoteEndedReason {
pub fn as_str(self) -> &'static str {
match self {
Self::SessionClosed => "session-closed",
Self::SessionExited => "session-exited",
Self::SessionUnavailable => "session-unavailable",
Self::HostRestarted => "host-restarted",
Self::HostShutdown => "host-shutdown",
Self::ClosedLocally => "closed-locally",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoteExitInfo {
pub code: Option<i32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RemoteViewState {
Pending,
Attached,
Failed,
}
impl RemoteViewState {
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Attached => "attached",
Self::Failed => "failed",
}
}
}
#[derive(Clone, Debug)]
pub struct RemoteLifecycleChanged {
pub session_id: String,
pub lifecycle_seq: u64,
pub device_id: String,
pub device_name: String,
pub state: RemoteLifecycleState,
pub reason: Option<RemoteEndedReason>,
pub exit: Option<RemoteExitInfo>,
pub attempt: u32,
pub next_retry_ms: Option<u64>,
pub last_contact_ms: Option<u64>,
}
#[derive(Clone, Debug)]
pub struct RemoteViewStateChanged {
pub session_id: String,
pub local_view_id: String,
pub view_state_seq: u64,
pub view_state: RemoteViewState,
pub attachment_epoch: Option<u64>,
pub read_write: Option<bool>,
pub error: Option<String>,
pub retryable: Option<bool>,
}
#[derive(Clone, Debug)]
pub struct RemoteViewRecord {
pub local_view_id: String,
pub view_state_seq: u64,
pub view_state: RemoteViewState,
pub attachment_epoch: Option<u64>,
pub read_write: Option<bool>,
pub error: Option<String>,
pub retryable: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RemoteController {
pub view_id: String,
pub control_epoch: u64,
}
#[derive(Clone, Debug)]
pub struct RemoteControlState {
pub session_id: String,
pub controller: Option<RemoteController>,
pub control_revision: u64,
pub cols: u16,
pub rows: u16,
pub layout_epoch: u64,
}
#[derive(Clone, Debug)]
pub enum RemoteControlOutcome {
Claimed(RemoteControlState),
Rejected(RemoteControlState),
}
#[derive(Clone, Copy, Debug)]
pub struct MeshReconnectConfig {
pub backoff_base: Duration,
pub backoff_cap: Duration,
pub backoff_floor: Duration,
pub suspend_after: Duration,
pub synchronize_timeout: Duration,
pub advertisement_fast_path: bool,
pub zombie_purge: bool,
pub heartbeat_idle: Duration,
pub heartbeat_fail: Duration,
}
impl Default for MeshReconnectConfig {
fn default() -> Self {
Self {
backoff_base: Duration::from_millis(500),
backoff_cap: Duration::from_secs(10),
backoff_floor: Duration::from_millis(250),
suspend_after: Duration::from_secs(10 * 60),
synchronize_timeout: Duration::from_secs(10),
advertisement_fast_path: true,
zombie_purge: true,
heartbeat_idle: Duration::from_secs(3),
heartbeat_fail: Duration::from_secs(6),
}
}
}
#[derive(Clone, Debug)]
pub struct RemoteSessionLifecycle {
pub session_id: String,
pub lifecycle_seq: u64,
pub device_id: String,
pub device_name: String,
pub state: RemoteLifecycleState,
pub reason: Option<RemoteEndedReason>,
pub exit: Option<RemoteExitInfo>,
pub attempt: u32,
pub next_retry_ms: Option<u64>,
pub last_contact_ms: Option<u64>,
pub views: Vec<RemoteViewRecord>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoteHostSummary {
pub device_id: String,
pub device_name: String,
pub online: bool,
pub protocol_major: u16,
pub protocol_minor: u16,
pub host_instance_id: String,
pub sessions: Vec<SharedSessionSummary>,
}
pub struct RemoteSessionOpen {
pub device_id: String,
pub remote_session_id: String,
pub cols: u16,
pub rows: u16,
pub owner_id: Option<String>,
pub frames: FrameHub,
pub text_engine: Arc<Mutex<TextEngine>>,
}
#[async_trait]
pub trait RemoteTerminalRuntime: Send + Sync {
fn subscribe_control(&self) -> broadcast::Receiver<RemoteControlChanged>;
fn subscribe_activity(&self) -> broadcast::Receiver<RemoteActivityChanged>;
async fn hosts(&self) -> Result<Vec<RemoteHostSummary>>;
async fn list_sessions(&self, device_id: &str) -> Result<Vec<SharedSessionSummary>>;
async fn open_session(&self, request: RemoteSessionOpen) -> Result<SessionSummary>;
async fn summaries(&self) -> Vec<SessionSummary>;
async fn summary(&self, session_id: &str) -> Option<SessionSummary>;
async fn attach_view(&self, session_id: &str, view_id: &str) -> Result<RemoteAttachment>;
async fn send_input(
&self,
session_id: &str,
view_id: &str,
attachment_epoch: u64,
input_sequence: u64,
operation: TunnelInput,
) -> Result<()>;
async fn claim_control(
&self,
session_id: &str,
view_id: &str,
attachment_epoch: u64,
cols: u16,
rows: u16,
) -> Result<RemoteControlClaim>;
async fn resize(&self, session_id: &str, view_id: &str, request: RemoteResize) -> Result<()>;
async fn selection_text(
&self,
session_id: &str,
view_id: &str,
request: RemoteSelection,
) -> Result<String>;
async fn refresh(&self, session_id: &str) -> Result<()>;
async fn detach_view(&self, session_id: &str, view_id: &str, attachment_epoch: u64);
async fn close_session(&self, session_id: &str) -> bool;
fn subscribe_lifecycle(&self) -> broadcast::Receiver<RemoteLifecycleChanged> {
closed_channel()
}
fn subscribe_view_state(&self) -> broadcast::Receiver<RemoteViewStateChanged> {
closed_channel()
}
async fn session_lifecycle(&self, _session_id: &str) -> Option<RemoteSessionLifecycle> {
None
}
async fn current_attachment(&self, _session_id: &str, _view_id: &str) -> Option<(u64, bool)> {
None
}
async fn reconnect_session(&self, _session_id: &str) -> Result<RemoteSessionLifecycle> {
unavailable()
}
async fn refresh_remote(&self, _session_id: &str) -> Result<()> {
unavailable()
}
fn subscribe_control_state(&self) -> broadcast::Receiver<RemoteControlState> {
closed_channel()
}
async fn control_state(&self, _session_id: &str) -> Option<RemoteControlState> {
None
}
async fn claim_control_at(
&self,
_session_id: &str,
_view_id: &str,
_attachment_epoch: u64,
_cols: u16,
_rows: u16,
_expected_control_revision: Option<u64>,
) -> Result<RemoteControlOutcome> {
unavailable()
}
async fn retry_view(&self, _session_id: &str, _view_id: &str) -> Result<RemoteViewRecord> {
unavailable()
}
async fn probe_connection(&self, _device_id: &str) -> Result<()> {
unavailable()
}
async fn offline_selection_text(
&self,
_session_id: &str,
_request: RemoteSelection,
) -> Result<String> {
unavailable()
}
}
fn closed_channel<T: Clone>() -> broadcast::Receiver<T> {
let (sender, receiver) = broadcast::channel(1);
drop(sender);
receiver
}
pub trait SessionStatusSource: Send + Sync {
fn session_status(&self, session_id: &str) -> SessionStatus;
}
#[derive(Clone)]
pub struct HostShutdownAnnouncer {
announced: tokio::sync::watch::Sender<bool>,
}
impl Default for HostShutdownAnnouncer {
fn default() -> Self {
Self::new()
}
}
impl HostShutdownAnnouncer {
pub fn new() -> Self {
Self {
announced: tokio::sync::watch::channel(false).0,
}
}
pub fn announce(&self) {
self.announced.send_replace(true);
}
pub fn announced(&self) -> bool {
*self.announced.borrow()
}
pub fn watch(&self) -> tokio::sync::watch::Receiver<bool> {
self.announced.subscribe()
}
}
#[async_trait]
pub trait TerminalMesh: Send {
fn runtime(&self) -> Arc<dyn RemoteTerminalRuntime>;
async fn serve(
self: Box<Self>,
registry: Registry,
host_config: tokio::sync::watch::Receiver<Arc<TerminalPresentationConfig>>,
) -> Result<()>;
fn set_session_status_source(&mut self, _source: Arc<dyn SessionStatusSource>) {}
fn shutdown_announcer(&self) -> HostShutdownAnnouncer {
HostShutdownAnnouncer::new()
}
}
#[derive(Default)]
pub(crate) struct NoRemoteRuntime;
fn unavailable<T>() -> Result<T> {
bail!("remote terminal transport is not configured")
}
#[async_trait]
impl RemoteTerminalRuntime for NoRemoteRuntime {
fn subscribe_control(&self) -> broadcast::Receiver<RemoteControlChanged> {
let (sender, receiver) = broadcast::channel(1);
drop(sender);
receiver
}
fn subscribe_activity(&self) -> broadcast::Receiver<RemoteActivityChanged> {
let (sender, receiver) = broadcast::channel(1);
drop(sender);
receiver
}
async fn hosts(&self) -> Result<Vec<RemoteHostSummary>> {
Ok(Vec::new())
}
async fn list_sessions(&self, _device_id: &str) -> Result<Vec<SharedSessionSummary>> {
unavailable()
}
async fn open_session(&self, _request: RemoteSessionOpen) -> Result<SessionSummary> {
unavailable()
}
async fn summaries(&self) -> Vec<SessionSummary> {
Vec::new()
}
async fn summary(&self, _session_id: &str) -> Option<SessionSummary> {
None
}
async fn attach_view(&self, _session_id: &str, _view_id: &str) -> Result<RemoteAttachment> {
unavailable()
}
async fn send_input(
&self,
_session_id: &str,
_view_id: &str,
_attachment_epoch: u64,
_input_sequence: u64,
_operation: TunnelInput,
) -> Result<()> {
unavailable()
}
async fn claim_control(
&self,
_session_id: &str,
_view_id: &str,
_attachment_epoch: u64,
_cols: u16,
_rows: u16,
) -> Result<RemoteControlClaim> {
unavailable()
}
async fn resize(
&self,
_session_id: &str,
_view_id: &str,
_request: RemoteResize,
) -> Result<()> {
unavailable()
}
async fn selection_text(
&self,
_session_id: &str,
_view_id: &str,
_request: RemoteSelection,
) -> Result<String> {
unavailable()
}
async fn refresh(&self, _session_id: &str) -> Result<()> {
unavailable()
}
async fn detach_view(&self, _session_id: &str, _view_id: &str, _attachment_epoch: u64) {}
async fn close_session(&self, _session_id: &str) -> bool {
false
}
}