use std::any::Any;
use std::collections::BTreeMap;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, OnceLock, RwLock, Weak};
use crate::BmuxClient;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachTarget {
raw: String,
scheme: Option<String>,
reference: String,
}
impl AttachTarget {
#[must_use]
pub fn parse(raw: impl Into<String>) -> Self {
let raw = raw.into();
let (scheme, reference) = raw.split_once("://").map_or_else(
|| (None, raw.clone()),
|(scheme, reference)| {
if valid_scheme(scheme) {
(Some(scheme.to_ascii_lowercase()), reference.to_string())
} else {
(None, raw.clone())
}
},
);
Self {
raw,
scheme,
reference,
}
}
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub fn scheme(&self) -> Option<&str> {
self.scheme.as_deref()
}
#[must_use]
pub fn reference(&self) -> &str {
&self.reference
}
}
fn valid_scheme(scheme: &str) -> bool {
let mut chars = scheme.chars();
chars
.next()
.is_some_and(|first| first.is_ascii_alphabetic())
&& chars.all(|value| value.is_ascii_alphanumeric() || matches!(value, '+' | '-' | '.'))
}
pub type AttachProviderFuture<'a, T> =
Pin<Box<dyn Future<Output = Result<T, AttachProviderError>> + Send + 'a>>;
pub type AttachEndpointConnectFuture<'a> =
Pin<Box<dyn Future<Output = Result<BmuxClient, String>> + Send + 'a>>;
pub trait AttachEndpointConnector: fmt::Debug + Send + Sync + 'static {
fn connect<'a>(
&'a self,
target: &'a str,
client_name: &'static str,
) -> AttachEndpointConnectFuture<'a>;
}
pub struct AttachProviderOpenContext {
pub fallback_client: Option<BmuxClient>,
pub endpoint_connector: Option<Arc<dyn AttachEndpointConnector>>,
}
impl fmt::Debug for AttachProviderOpenContext {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AttachProviderOpenContext")
.field("has_fallback_client", &self.fallback_client.is_some())
.field("has_endpoint_connector", &self.endpoint_connector.is_some())
.finish()
}
}
pub trait ResolvedAttachTarget: fmt::Debug + Send + Sync + 'static {
fn provider_id(&self) -> &str;
fn as_any(&self) -> &dyn Any;
}
#[derive(Debug)]
pub enum AttachProviderBackend {
Legacy(BmuxClient),
Session(Box<dyn crate::AttachSession>),
}
#[derive(Debug)]
pub struct AttachProviderSession {
pub backend: AttachProviderBackend,
pub target: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AttachProviderError {
#[error("attach provider '{provider_id}' rejected target '{target}': {reason}")]
InvalidTarget {
provider_id: String,
target: String,
reason: String,
},
#[error("attach provider '{provider_id}' failed opening target: {reason}")]
OpenFailed { provider_id: String, reason: String },
}
pub trait AttachProvider: fmt::Debug + Send + Sync + 'static {
fn id(&self) -> &str;
fn priority(&self) -> i32 {
0
}
fn supports(&self, target: &AttachTarget) -> bool;
fn requires_fallback_client(&self) -> bool {
false
}
fn allows_retarget(&self) -> bool {
true
}
fn observe_viewport<'a>(
&'a self,
_client: &'a mut crate::StreamingBmuxClient,
_target: &'a str,
_fallback: crate::AttachProviderViewport,
) -> AttachProviderFuture<'a, crate::AttachProviderViewport> {
Box::pin(async move {
Err(AttachProviderError::OpenFailed {
provider_id: self.id().to_string(),
reason: "non-sizing observation is unsupported".to_string(),
})
})
}
fn resolve(
&self,
target: &AttachTarget,
) -> Result<Arc<dyn ResolvedAttachTarget>, AttachProviderError>;
fn open(
&self,
resolved: Arc<dyn ResolvedAttachTarget>,
resume: Option<crate::AttachResumeState>,
context: AttachProviderOpenContext,
) -> AttachProviderFuture<'_, AttachProviderSession>;
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AttachProviderRegistrationError {
#[error("attach provider id cannot be empty")]
EmptyId,
#[error("attach provider id cannot contain surrounding whitespace: '{id}'")]
InvalidId { id: String },
#[error("attach provider '{id}' is already registered")]
DuplicateId { id: String },
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AttachProviderResolutionError {
#[error("no attach provider accepts target '{target}'")]
NoProvider { target: String },
#[error("attach target '{target}' is ambiguous between providers: {providers:?}")]
Ambiguous {
target: String,
providers: Vec<String>,
},
}
#[derive(Debug, Default)]
struct RegistryState {
providers: BTreeMap<String, Arc<dyn AttachProvider>>,
}
#[derive(Debug, Default)]
pub struct AttachProviderRegistry {
state: RwLock<RegistryState>,
}
impl AttachProviderRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(
self: &Arc<Self>,
provider: Arc<dyn AttachProvider>,
) -> Result<AttachProviderRegistration, AttachProviderRegistrationError> {
let id = provider.id().to_string();
if id.is_empty() {
return Err(AttachProviderRegistrationError::EmptyId);
}
if id.trim() != id {
return Err(AttachProviderRegistrationError::InvalidId { id });
}
let mut state = self
.state
.write()
.expect("attach provider registry poisoned");
if state.providers.contains_key(&id) {
return Err(AttachProviderRegistrationError::DuplicateId { id });
}
state.providers.insert(id.clone(), provider);
drop(state);
Ok(AttachProviderRegistration {
registry: Arc::downgrade(self),
id: Some(id),
})
}
pub fn resolve(
&self,
target: &AttachTarget,
) -> Result<Arc<dyn AttachProvider>, AttachProviderResolutionError> {
let mut matches = {
let state = self
.state
.read()
.expect("attach provider registry poisoned");
state
.providers
.values()
.filter(|provider| provider.supports(target))
.cloned()
.collect::<Vec<_>>()
};
if matches.is_empty() {
return Err(AttachProviderResolutionError::NoProvider {
target: target.raw().to_string(),
});
}
matches.sort_by(|left, right| {
right
.priority()
.cmp(&left.priority())
.then_with(|| left.id().cmp(right.id()))
});
let priority = matches[0].priority();
let winners = matches
.iter()
.take_while(|provider| provider.priority() == priority)
.collect::<Vec<_>>();
if winners.len() > 1 {
return Err(AttachProviderResolutionError::Ambiguous {
target: target.raw().to_string(),
providers: winners
.iter()
.map(|provider| provider.id().to_string())
.collect(),
});
}
Ok(Arc::clone(&matches[0]))
}
#[must_use]
pub fn provider_ids(&self) -> Vec<String> {
self.state
.read()
.expect("attach provider registry poisoned")
.providers
.keys()
.cloned()
.collect()
}
}
#[derive(Debug)]
pub struct AttachProviderRegistration {
registry: Weak<AttachProviderRegistry>,
id: Option<String>,
}
impl AttachProviderRegistration {
pub fn forget(mut self) {
let _ = self.id.take();
}
}
impl Drop for AttachProviderRegistration {
fn drop(&mut self) {
let (Some(registry), Some(id)) = (self.registry.upgrade(), self.id.take()) else {
return;
};
registry
.state
.write()
.expect("attach provider registry poisoned")
.providers
.remove(&id);
}
}
#[must_use]
pub fn global_attach_provider_registry() -> Arc<AttachProviderRegistry> {
static REGISTRY: OnceLock<Arc<AttachProviderRegistry>> = OnceLock::new();
Arc::clone(REGISTRY.get_or_init(|| Arc::new(AttachProviderRegistry::new())))
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct TestResolvedTarget {
provider_id: &'static str,
}
impl ResolvedAttachTarget for TestResolvedTarget {
fn provider_id(&self) -> &str {
self.provider_id
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Debug)]
struct SchemeProvider {
id: &'static str,
scheme: Option<&'static str>,
priority: i32,
}
impl AttachProvider for SchemeProvider {
fn id(&self) -> &str {
self.id
}
fn priority(&self) -> i32 {
self.priority
}
fn supports(&self, target: &AttachTarget) -> bool {
target.scheme() == self.scheme
}
fn resolve(
&self,
_target: &AttachTarget,
) -> Result<Arc<dyn ResolvedAttachTarget>, AttachProviderError> {
Ok(Arc::new(TestResolvedTarget {
provider_id: self.id,
}))
}
fn open(
&self,
_resolved: Arc<dyn ResolvedAttachTarget>,
_resume: Option<crate::AttachResumeState>,
_context: AttachProviderOpenContext,
) -> AttachProviderFuture<'_, AttachProviderSession> {
Box::pin(async move {
Err(AttachProviderError::OpenFailed {
provider_id: self.id.to_string(),
reason: "test provider does not open clients".to_string(),
})
})
}
}
#[test]
fn target_parser_normalizes_valid_schemes_and_preserves_bare_targets() {
let target = AttachTarget::parse("SYNTHETIC://workspace/main");
assert_eq!(target.scheme(), Some("synthetic"));
assert_eq!(target.reference(), "workspace/main");
assert_eq!(target.raw(), "SYNTHETIC://workspace/main");
let bare = AttachTarget::parse("main");
assert_eq!(bare.scheme(), None);
assert_eq!(bare.reference(), "main");
let invalid = AttachTarget::parse("1bad://main");
assert_eq!(invalid.scheme(), None);
assert_eq!(invalid.reference(), "1bad://main");
}
#[test]
fn resolves_one_matching_provider() {
let registry = Arc::new(AttachProviderRegistry::new());
let _registration = registry
.register(Arc::new(SchemeProvider {
id: "synthetic",
scheme: Some("synthetic"),
priority: 10,
}))
.expect("register provider");
let provider = registry
.resolve(&AttachTarget::parse("synthetic://main"))
.expect("resolve provider");
assert_eq!(provider.id(), "synthetic");
}
#[test]
fn higher_priority_provider_wins_and_ties_are_rejected() {
let registry = Arc::new(AttachProviderRegistry::new());
let _low = registry
.register(Arc::new(SchemeProvider {
id: "low",
scheme: Some("synthetic"),
priority: 1,
}))
.unwrap();
let high = registry
.register(Arc::new(SchemeProvider {
id: "high",
scheme: Some("synthetic"),
priority: 2,
}))
.unwrap();
assert_eq!(
registry
.resolve(&AttachTarget::parse("synthetic://main"))
.unwrap()
.id(),
"high"
);
drop(high);
let _tie = registry
.register(Arc::new(SchemeProvider {
id: "tie",
scheme: Some("synthetic"),
priority: 1,
}))
.unwrap();
assert!(matches!(
registry.resolve(&AttachTarget::parse("synthetic://main")),
Err(AttachProviderResolutionError::Ambiguous { providers, .. })
if providers == ["low", "tie"]
));
}
#[test]
fn duplicate_ids_are_rejected_and_drop_unregisters() {
let registry = Arc::new(AttachProviderRegistry::new());
let registration = registry
.register(Arc::new(SchemeProvider {
id: "provider",
scheme: None,
priority: 0,
}))
.unwrap();
assert!(matches!(
registry.register(Arc::new(SchemeProvider {
id: "provider",
scheme: Some("other"),
priority: 0,
})),
Err(AttachProviderRegistrationError::DuplicateId { .. })
));
drop(registration);
assert!(registry.provider_ids().is_empty());
}
#[test]
fn no_match_is_structured() {
let registry = AttachProviderRegistry::new();
assert!(matches!(
registry.resolve(&AttachTarget::parse("unknown://main")),
Err(AttachProviderResolutionError::NoProvider { target })
if target == "unknown://main"
));
}
}