use std::sync::Arc;
use std::time::SystemTime;
use ahp_types::actions::ActionEnvelope;
use ahp_types::state::{AgentInfo, RootState, SessionSummary, TerminalInfo};
use thiserror::Error;
use tokio::sync::{broadcast, Mutex};
use crate::{Client, ClientConfig, ClientError, DispatchHandle, SubscriptionEvent};
use super::factory::HostTransportFactory;
use super::policy::ReconnectPolicy;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct HostId(String);
impl HostId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for HostId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for HostId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for HostId {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum HostState {
Disconnected,
Connecting,
Connected,
Reconnecting {
attempt: u32,
},
Failed {
error: Arc<ClientError>,
},
}
impl PartialEq for HostState {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(HostState::Disconnected, HostState::Disconnected) => true,
(HostState::Connecting, HostState::Connecting) => true,
(HostState::Connected, HostState::Connected) => true,
(HostState::Reconnecting { attempt: a }, HostState::Reconnecting { attempt: b }) => {
a == b
}
_ => false,
}
}
}
pub struct HostConfig {
pub id: HostId,
pub label: String,
pub client_id: Option<String>,
pub initial_subscriptions: Vec<String>,
pub client_config: ClientConfig,
pub transport_factory: Arc<dyn HostTransportFactory>,
pub reconnect_policy: ReconnectPolicy,
}
impl HostConfig {
pub fn new(
id: impl Into<HostId>,
label: impl Into<String>,
transport_factory: impl HostTransportFactory,
) -> Self {
Self {
id: id.into(),
label: label.into(),
client_id: None,
initial_subscriptions: vec![ahp_types::ROOT_RESOURCE_URI.to_string()],
client_config: ClientConfig::default(),
transport_factory: Arc::new(transport_factory),
reconnect_policy: ReconnectPolicy::default(),
}
}
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn with_initial_subscriptions(mut self, uris: Vec<String>) -> Self {
self.initial_subscriptions = uris;
self
}
pub fn with_client_config(mut self, config: ClientConfig) -> Self {
self.client_config = config;
self
}
pub fn with_reconnect_policy(mut self, policy: ReconnectPolicy) -> Self {
self.reconnect_policy = policy;
self
}
}
impl std::fmt::Debug for HostConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HostConfig")
.field("id", &self.id)
.field("label", &self.label)
.field("client_id", &self.client_id)
.field("initial_subscriptions", &self.initial_subscriptions)
.field("reconnect_policy", &self.reconnect_policy)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct HostHandle {
pub id: HostId,
pub label: String,
pub client_id: String,
pub state: HostState,
pub last_error: Option<Arc<ClientError>>,
pub last_connected_at: Option<SystemTime>,
pub protocol_version: Option<String>,
pub server_seq: i64,
pub default_directory: Option<String>,
pub agents: Vec<AgentInfo>,
pub active_sessions: Option<i64>,
pub terminals: Option<Vec<TerminalInfo>>,
pub subscriptions: Vec<String>,
pub completion_trigger_characters: Vec<String>,
pub session_summaries: Vec<SessionSummary>,
pub generation: u64,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum HostError {
#[error("no host registered with id {0}")]
UnknownHost(HostId),
#[error("a host with id {0} is already registered")]
DuplicateHost(HostId),
#[error("host {host} reconnected (generation {handle_generation} -> {current_generation}); request a fresh client handle")]
HostReconnected {
host: HostId,
handle_generation: u64,
current_generation: u64,
},
#[error("host {0} runtime is no longer active")]
HostShutDown(HostId),
#[error("client id store error for host {host}: {error}")]
ClientIdStore {
host: HostId,
#[source]
error: std::io::Error,
},
#[error(transparent)]
Client(#[from] ClientError),
}
#[derive(Clone)]
pub struct HostClientHandle {
pub(super) host_id: HostId,
pub(super) generation: u64,
pub(super) client: Client,
pub(super) shared: Arc<HostShared>,
}
impl HostClientHandle {
pub fn host_id(&self) -> &HostId {
&self.host_id
}
pub fn generation(&self) -> u64 {
self.generation
}
pub async fn check_alive(&self) -> Result<(), HostError> {
let state = self.shared.lock().await;
if state.generation != self.generation {
return Err(HostError::HostReconnected {
host: self.host_id.clone(),
handle_generation: self.generation,
current_generation: state.generation,
});
}
Ok(())
}
pub async fn dispatch(
&self,
channel: ahp_types::common::Uri,
action: ahp_types::actions::StateAction,
) -> Result<DispatchHandle, HostError> {
self.check_alive().await?;
Ok(self.client.dispatch(channel, action).await?)
}
pub async fn request<P, R>(&self, method: &str, params: P) -> Result<R, HostError>
where
P: serde::Serialize,
R: serde::de::DeserializeOwned,
{
self.check_alive().await?;
Ok(self.client.request(method, params).await?)
}
pub fn raw_client(&self) -> &Client {
&self.client
}
}
impl std::fmt::Debug for HostClientHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HostClientHandle")
.field("host_id", &self.host_id)
.field("generation", &self.generation)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct HostSubscriptionEvent {
pub host_id: HostId,
pub channel: String,
pub event: SubscriptionEvent,
}
pub struct HostSubscriptionStream {
pub(super) rx: broadcast::Receiver<HostSubscriptionEvent>,
}
impl HostSubscriptionStream {
pub async fn recv(&mut self) -> Option<HostSubscriptionEvent> {
loop {
match self.rx.recv().await {
Ok(ev) => return Some(ev),
Err(broadcast::error::RecvError::Closed) => return None,
Err(broadcast::error::RecvError::Lagged(_)) => continue,
}
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum HostEvent {
Added {
host_id: HostId,
},
StateChanged {
host_id: HostId,
state: HostState,
last_error: Option<Arc<ClientError>>,
},
Connected {
host_id: HostId,
generation: u64,
},
Removed {
host_id: HostId,
},
}
pub struct HostEventStream {
pub(super) rx: broadcast::Receiver<HostEvent>,
}
impl HostEventStream {
pub async fn recv(&mut self) -> Option<HostEvent> {
loop {
match self.rx.recv().await {
Ok(ev) => return Some(ev),
Err(broadcast::error::RecvError::Closed) => return None,
Err(broadcast::error::RecvError::Lagged(_)) => continue,
}
}
}
}
#[derive(Debug, Clone)]
pub struct HostedSessionSummary {
pub host_id: HostId,
pub host_label: String,
pub summary: SessionSummary,
}
#[derive(Debug, Clone)]
pub struct HostedAgent {
pub host_id: HostId,
pub host_label: String,
pub agent: AgentInfo,
}
pub(super) struct HostShared {
inner: Mutex<HostInternal>,
}
impl HostShared {
pub(super) fn new(initial: HostInternal) -> Arc<Self> {
Arc::new(Self {
inner: Mutex::new(initial),
})
}
pub(super) async fn lock(&self) -> tokio::sync::MutexGuard<'_, HostInternal> {
self.inner.lock().await
}
}
pub(super) struct HostInternal {
pub(super) id: HostId,
pub(super) label: String,
pub(super) client_id: String,
pub(super) state: HostState,
pub(super) last_error: Option<Arc<ClientError>>,
pub(super) last_connected_at: Option<SystemTime>,
pub(super) protocol_version: Option<String>,
pub(super) server_seq: i64,
pub(super) default_directory: Option<String>,
pub(super) root_state: RootState,
pub(super) subscriptions: Vec<String>,
pub(super) completion_trigger_characters: Vec<String>,
pub(super) session_summaries: std::collections::BTreeMap<String, SessionSummary>,
pub(super) generation: u64,
pub(super) current_client: Option<Client>,
}
impl HostInternal {
pub(super) fn snapshot(&self) -> HostHandle {
HostHandle {
id: self.id.clone(),
label: self.label.clone(),
client_id: self.client_id.clone(),
state: self.state.clone(),
last_error: self.last_error.clone(),
last_connected_at: self.last_connected_at,
protocol_version: self.protocol_version.clone(),
server_seq: self.server_seq,
default_directory: self.default_directory.clone(),
agents: self.root_state.agents.clone(),
active_sessions: self.root_state.active_sessions,
terminals: self.root_state.terminals.clone(),
subscriptions: self.subscriptions.clone(),
completion_trigger_characters: self.completion_trigger_characters.clone(),
session_summaries: self.session_summaries.values().cloned().collect(),
generation: self.generation,
}
}
}
pub(super) fn server_seq_from_envelope(env: &ActionEnvelope) -> i64 {
env.server_seq as i64
}
pub(super) fn generate_client_id() -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut hasher = DefaultHasher::new();
let mut out = [0u8; 16];
let now_nanos = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
for i in 0..2 {
n.hash(&mut hasher);
now_nanos.hash(&mut hasher);
i.hash(&mut hasher);
let bytes = hasher.finish().to_be_bytes();
out[i * 8..(i + 1) * 8].copy_from_slice(&bytes);
}
out[6] = (out[6] & 0x0f) | 0x40;
out[8] = (out[8] & 0x3f) | 0x80;
format!(
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
u32::from_be_bytes([out[0], out[1], out[2], out[3]]),
u16::from_be_bytes([out[4], out[5]]),
u16::from_be_bytes([out[6], out[7]]),
u16::from_be_bytes([out[8], out[9]]),
u64::from_be_bytes([0, 0, out[10], out[11], out[12], out[13], out[14], out[15]])
& 0xffff_ffff_ffff,
)
}